Skip to content

feat(core/menu): Implement w3c menu pattern for ix-menu#2573

Merged
nuke-ellington merged 44 commits into
mainfrom
feat/4304-menu-a11y
Jul 7, 2026
Merged

feat(core/menu): Implement w3c menu pattern for ix-menu#2573
nuke-ellington merged 44 commits into
mainfrom
feat/4304-menu-a11y

Conversation

@lzeiml

@lzeiml lzeiml commented May 28, 2026

Copy link
Copy Markdown
Collaborator

IX-4304

💡 What is the current behavior?

Currently ix-menu does not have proper keyboard-navigation according to w3c menubar pattern: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/

🆕 What is the new behavior?

Implements the w3c keyboard-navigation and roles for better screenreader support.

🏁 Checklist

A pull request can only be merged if all of these conditions are met (where applicable):

  • 🦮 Accessibility (a11y) features were implemented
  • 🗺️ Internationalization (i18n) - no hard coded strings
  • 📲 Responsiveness - components handle viewport changes and content overflow gracefully
  • 📕 Add or update a Storybook story
  • 📄 Documentation was reviewed/updated siemens/ix-docs
  • 🧪 Unit tests were added/updated and pass (pnpm test)
  • 📸 Visual regression tests were added/updated and pass (Guide)
  • 🧐 Static code analysis passes (pnpm lint)
  • 🏗️ Successful compilation (pnpm build, changes pushed)

👨‍💻 Help & support

Summary by CodeRabbit

  • New Features
    • Added accessibility i18n props for ix-menu and enhanced menubar support with roving tabIndex, improved Arrow/Home/End navigation, and utility controls grouping.
    • Introduced public setTabIndex() APIs for menu items/categories and improved category labeling support; added typings for ix-playground.
  • Bug Fixes / Accessibility
    • Refined dropdown open/close and focus restore behavior, updated ARIA roles/attributes (e.g., tooltip, menu item state), and improved keyboard activation handling.
    • Updated expand/collapse icons to use aria-expanded.
  • Tests
    • Updated Playwright and visual e2e tests to match updated menuitem roles and interactions.
  • Chores
    • Refreshed public typings (including Angular proxy inputs) and added an accessibility patch changeset.

@changeset-bot

changeset-bot Bot commented May 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b58fbcf

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@siemens/ix Minor
@siemens/ix-angular Minor
@siemens/ix-docs Minor
@siemens/ix-react Minor
@siemens/ix-vue Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented May 28, 2026

Copy link
Copy Markdown

Deploy Preview for ix-storybook ready!

Name Link
🔨 Latest commit b58fbcf
🔍 Latest deploy log https://app.netlify.com/projects/ix-storybook/deploys/6a4cea889e3585000841782f
😎 Deploy Preview https://deploy-preview-2573--ix-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces keyboard navigation and a roving tab index mechanism for the ix-menu component, updating ix-menu-item and ix-menu-category to support dynamic tab index configuration. A critical review comment points out that using closest('ix-menu') to identify direct menu children is too broad and will break focusability for nested items in slots like ix-menu-settings, suggesting a direct parent check instead.

Comment thread packages/core/src/components/menu-item/menu-item.tsx Outdated
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements menubar keyboard navigation and ARIA updates for ix-menu, plus related ix-menu-item and ix-menu-category focus behavior changes, typings, styling, tests, fixtures, and a changeset.

Changes

Menubar accessibility implementation

Layer / File(s) Summary
Public API and typings
.changeset/fair-falcons-search.md, packages/core/src/components.d.ts, packages/angular/src/components.ts
Adds menu i18n props, setTabIndex() methods, menuCategoryLabel, and ix-playground typings across Stencil, JSX, Angular, and changeset surfaces.
Menu navigation and layout
packages/core/src/components/menu/menu.tsx, packages/core/src/components/menu/menu.scss, packages/core/src/components/toast/tests/toast.ct.ts, testing/visual-testing/tests/menu-about-news/menu-about-news.e2e.ts, testing/visual-testing/tests/menu/link/index.html
Turns ix-menu into a vertical menubar with roving tabindex, overflow tracking, utility-item grouping, focus-visible styling, updated test locators, and revised menu link markup.
Menu item ARIA and role handling
packages/core/src/components/menu-item/menu-item.tsx
Refactors ix-menu-item to inherit ARIA attributes, compute effective roles and labels, manage host tab index, and update rendering for menu/category context.
Category dropdown focus and labeling
packages/core/src/components/menu-category/menu-category.tsx, packages/core/src/components/menu-category/test/menu-category.ct.ts
Adds generated category IDs, dropdown show/hide focus handling, category-label plumbing, and arrow-key navigation coverage for the dropdown regression test.

🎯 4 (Complex) | ⏱️ ~60 minutes

🐰 I hopped through the menu with cheer,
Tabs turned to arrows, crisp and clear.
Labels and focus now lead the way,
Menubar moonbeams guide the day. ✨

  • danielleroux
  • nuke-ellington
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: updating ix-menu to implement the W3C menu pattern.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/4304-menu-a11y

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lzeiml lzeiml marked this pull request as ready for review June 12, 2026 07:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/core/src/components.d.ts (1)

2595-2690: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the required @since docs for the new public API.

setTabIndex and menuCategoryLabel are new externally visible API surface, but these generated typings don’t carry the version metadata this repo requires for new props/methods.

As per coding guidelines, new public API additions must include JSDoc with a @since tag in the source declarations so the generated types stay consistent. ``

Also applies to: 9107-9170, 11757-11771

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components.d.ts` around lines 2595 - 2690, The generated
typings are missing `@since` tags for newly added public API; add JSDoc `@since`
entries to the source declarations for the methods/props named setTabIndex and
menuCategoryLabel so the generator emits the version metadata. Locate the
component/source declaration files that define the IxMenuCategory/IxMenuItem
APIs (the functions/properties setTabIndex and menuCategoryLabel) and prepend
appropriate /** `@since` x.y.z */ JSDoc to each symbol (method signature for
setTabIndex and the menuCategoryLabel prop declaration) using the correct
release version.

Source: Coding guidelines

packages/core/src/components/menu-category/test/menu-category.ct.ts (1)

9-9: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing accessibility test using makeAxeBuilder.

Per coding guidelines, every .ct.ts file must include an 'accessibility' test using makeAxeBuilder to verify that accessibilityScanResults.violations returns an empty array. This test file has a 'renders' test but is missing the required accessibility test.

💡 Suggested accessibility test
import { makeAxeBuilder } from '`@utils/test`';

regressionTest('accessibility', async ({ mount, page }) => {
  await mount(`
    <ix-application>
      <ix-menu>
        <ix-menu-category label="Category label">
          <ix-menu-item>Test Item 1</ix-menu-item>
          <ix-menu-item>Test Item 2</ix-menu-item>
        </ix-menu-category>
      </ix-menu>
    </ix-application>
  `);

  const element = page.locator('ix-menu-category');
  await expect(element).toHaveClass(/hydrated/);

  const results = await makeAxeBuilder({ page }).analyze();
  expect(results.violations).toHaveLength(0);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/menu-category/test/menu-category.ct.ts` at line
9, Add a new regressionTest named "accessibility" to menu-category.ct.ts that
uses makeAxeBuilder to scan the mounted component; specifically, mount the same
markup used by the existing "renders" test (an ix-application with ix-menu and
ix-menu-category containing two ix-menu-item children), await the component to
hydrate via page.locator('ix-menu-category'), run makeAxeBuilder({ page
}).analyze(), and assert that results.violations has length 0 using expect;
ensure you import makeAxeBuilder from '`@utils/test`' and keep the test signature
consistent with the existing regressionTest(async ({ mount, page }) => { ... }).

Source: Coding guidelines

packages/core/src/components/toast/tests/toast.ct.ts (2)

19-171: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the required axe accessibility regression for this .ct.ts file.

This file has a 'renders' test, but it still lacks the mandatory 'accessibility' test using makeAxeBuilder() and an empty accessibilityScanResults.violations assertion. That leaves the menu-role change in this path uncovered by the repo’s required a11y gate. As per coding guidelines, "Ensure accessibility is tested with component tests using axe, verifying that accessibilityScanResults.violations returns an empty array", "Every test file should include an 'accessibility' test using makeAxeBuilder", and "Every test file should include a 'renders' test verifying basic hydration".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/toast/tests/toast.ct.ts` around lines 19 - 171,
Add a new regressionTest named 'accessibility' that mounts the component (reuse
the same mount pattern used in the 'renders' test), uses makeAxeBuilder(page) to
run an axe scan (include the toast element/selector), stores the result in
accessibilityScanResults, and assert accessibilityScanResults.violations is an
empty array; reference the existing regressionTest helper and makeAxeBuilder,
and place the test alongside the other tests in this file so the repo's a11y
gate is satisfied.

Source: Coding guidelines


71-72: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace the fixed sleeps with state-based waits.

These waitForTimeout(...) calls make the toast tests timing-sensitive and violate the component-test rules. Please wait on observable state instead—e.g. toast/menu visibility, attribute changes, or expect.poll() for the pause/resume API—so the test follows the component rather than the clock. As per coding guidelines, "Don't use arbitrary timeouts or waitForTimeout in tests" and "Use expect.poll() when a value requires async resolution or retries, but prefer Playwright's built-in auto-retry assertions whenever possible".

Also applies to: 107-108, 115-116

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/toast/tests/toast.ct.ts` around lines 71 - 72,
The tests currently use page.waitForTimeout(...) (e.g., the fixed 500ms sleeps)
which makes them timing-sensitive; replace each sleep with state-based waits
that observe the component: use Playwright's auto-retry assertions like await
expect(menuLocator).toBeVisible() / toBeHidden() or await
expect(toastLocator).toHaveAttribute(...) for attribute changes, and use
expect.poll() for the pause/resume API checks instead of fixed timeouts; update
the places calling page.waitForTimeout to wait on the toast/menu locators or the
pause/resume observable state (referencing the existing page.waitForTimeout
calls, the toast/menu locators, and the pause/resume checks) so tests wait on
component state not the clock.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/fair-falcons-search.md:
- Line 5: Update the changeset description string that currently reads "Improved
**ix-menu** accessibility by implementing the w3c menubar pattern, adding better
keyboard navigation and screenreader suppport." — fix the typo "suppport" →
"support" and capitalize the acronym "w3c" → "W3C" so the sentence becomes
"Improved **ix-menu** accessibility by implementing the W3C menubar pattern,
adding better keyboard navigation and screenreader support." Reference the same
changeset entry text in .changeset/fair-falcons-search.md to make the edits.
- Around line 1-5: The changeset for ix-menu is marked as patch but the
component adds new public API (new `@Prop/`@Event/@Method and exported interfaces
under packages/core/src/components/menu/*), so update
.changeset/fair-falcons-search.md to change the release type from patch to minor
and clearly list the `@siemens/ix` package; only add a "Fixes `#4304`" footer if the
PR/commit is actually linked to issue `#4304` (confirm the PR/issue connection
before adding that line).

In `@packages/core/src/components/menu-category/menu-category.tsx`:
- Around line 34-38: The helper createSequentialId currently increments its
parameter (sequenceId++) and callers also pass a post-incremented variable,
causing a redundant mutation; change createSequentialId to not mutate its input
(return `${prefix}-${sequenceId}`) and leave call-sites such as the usage with
categorySequenceId in menu-category.tsx and similar in menu-item.tsx using
sequenceId++ intact so the increment only happens at the call site; update the
createSequentialId implementation (and any docs/tests) to reflect the
non-mutating behavior.

In `@packages/core/src/components/menu-item/menu-item.tsx`:
- Around line 130-134: The new public API surface includes setTabIndex() (the
async method decorated with `@Method`() that sets hostTabIndex) but lacks
JSDoc/@since; either make it intentionally public by adding a JSDoc block above
setTabIndex() that documents the method and includes a `@since` tag with the
correct version, description and intended usage, or if it is internal plumbing
for roving tabindex, remove the `@Method`() decorator (and keep the method private
or rename to _setTabIndex) so it does not appear on the Stencil public surface;
update references to setTabIndex accordingly.
- Around line 218-233: getAriaLabel can produce "undefined …" when neither label
nor menuCategoryLabel is set; update getAriaLabel to compute a safe baseLabel
(e.g., const baseLabel = this.label ?? this.menuCategoryLabel ??
this.hostElement.textContent?.trim()) and use that instead of this.label when
building the distinct-tooltip branch; if baseLabel is falsy, return just
this.tooltipText (or undefined if tooltipText is also falsy) so you never
concatenate "undefined" with the tooltipText. Ensure changes are made inside the
getAriaLabel method and reference inheritAriaAttributes, tooltipText, label,
menuCategoryLabel, and hostElement.textContent.

In `@packages/core/src/components/menu/menu.tsx`:
- Around line 639-666: Fix the spelling typo in the comment above
updateMenuItemPositionMetadata: change "sepparated" to "separated" to improve
readability; locate the comment that reads "// item positions and menu size has
to be set manually because slotted items and utility controls are sepparated
into two groups" and update it accordingly (function:
updateMenuItemPositionMetadata, variable: allMenuItems).
- Around line 753-756: The switch branch handling 'End' indexes the last element
with items[items.length - 1]; change it to use the modern .at(-1) accessor for
clarity and safety: update the call to this.updateRovingTabIndex(items,
items.at(-1) ? items.length - 1 : items.length - 1) (or keep the index logic but
use items.at(-1) for retrieving the element to focus) and replace
items[items.length - 1].focus() with const last = items.at(-1); if (last) {
this.updateRovingTabIndex(items, items.length - 1); last.focus(); } to avoid
manual length arithmetic and null access in the case handlers.

---

Outside diff comments:
In `@packages/core/src/components.d.ts`:
- Around line 2595-2690: The generated typings are missing `@since` tags for newly
added public API; add JSDoc `@since` entries to the source declarations for the
methods/props named setTabIndex and menuCategoryLabel so the generator emits the
version metadata. Locate the component/source declaration files that define the
IxMenuCategory/IxMenuItem APIs (the functions/properties setTabIndex and
menuCategoryLabel) and prepend appropriate /** `@since` x.y.z */ JSDoc to each
symbol (method signature for setTabIndex and the menuCategoryLabel prop
declaration) using the correct release version.

In `@packages/core/src/components/menu-category/test/menu-category.ct.ts`:
- Line 9: Add a new regressionTest named "accessibility" to menu-category.ct.ts
that uses makeAxeBuilder to scan the mounted component; specifically, mount the
same markup used by the existing "renders" test (an ix-application with ix-menu
and ix-menu-category containing two ix-menu-item children), await the component
to hydrate via page.locator('ix-menu-category'), run makeAxeBuilder({ page
}).analyze(), and assert that results.violations has length 0 using expect;
ensure you import makeAxeBuilder from '`@utils/test`' and keep the test signature
consistent with the existing regressionTest(async ({ mount, page }) => { ... }).

In `@packages/core/src/components/toast/tests/toast.ct.ts`:
- Around line 19-171: Add a new regressionTest named 'accessibility' that mounts
the component (reuse the same mount pattern used in the 'renders' test), uses
makeAxeBuilder(page) to run an axe scan (include the toast element/selector),
stores the result in accessibilityScanResults, and assert
accessibilityScanResults.violations is an empty array; reference the existing
regressionTest helper and makeAxeBuilder, and place the test alongside the other
tests in this file so the repo's a11y gate is satisfied.
- Around line 71-72: The tests currently use page.waitForTimeout(...) (e.g., the
fixed 500ms sleeps) which makes them timing-sensitive; replace each sleep with
state-based waits that observe the component: use Playwright's auto-retry
assertions like await expect(menuLocator).toBeVisible() / toBeHidden() or await
expect(toastLocator).toHaveAttribute(...) for attribute changes, and use
expect.poll() for the pause/resume API checks instead of fixed timeouts; update
the places calling page.waitForTimeout to wait on the toast/menu locators or the
pause/resume observable state (referencing the existing page.waitForTimeout
calls, the toast/menu locators, and the pause/resume checks) so tests wait on
component state not the clock.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0b8b7e6-499a-484c-8a08-0f77eb6d5033

📥 Commits

Reviewing files that changed from the base of the PR and between 7c34939 and 7cdff0b.

⛔ Files ignored due to path filters (4)
  • packages/react/src/components/components.server.ts is excluded by !packages/react/src/components/**
  • packages/vue/src/components/ix-menu-item.ts is excluded by !packages/vue/src/components/**
  • testing/visual-testing/__screenshots__/tests/menu/menu.e2e.ts/menu-link---should-render-menu-items-with-a-href-1-chromium---classic-dark-linux.png is excluded by !**/*.png
  • testing/visual-testing/__screenshots__/tests/menu/menu.e2e.ts/menu-link---should-render-menu-items-with-a-href-1-chromium---classic-light-linux.png is excluded by !**/*.png
📒 Files selected for processing (10)
  • .changeset/fair-falcons-search.md
  • packages/core/src/components.d.ts
  • packages/core/src/components/menu-category/menu-category.tsx
  • packages/core/src/components/menu-category/test/menu-category.ct.ts
  • packages/core/src/components/menu-item/menu-item.tsx
  • packages/core/src/components/menu/menu.scss
  • packages/core/src/components/menu/menu.tsx
  • packages/core/src/components/toast/tests/toast.ct.ts
  • testing/visual-testing/tests/menu-about-news/menu-about-news.e2e.ts
  • testing/visual-testing/tests/menu/link/index.html

Comment thread .changeset/fair-falcons-search.md Outdated
Comment thread .changeset/fair-falcons-search.md Outdated
Comment thread packages/core/src/components/menu-category/menu-category.tsx
Comment thread packages/core/src/components/menu-item/menu-item.tsx
Comment thread packages/core/src/components/menu-item/menu-item.tsx
Comment thread packages/core/src/components/menu/menu.tsx Outdated
Comment thread packages/core/src/components/menu/menu.tsx
@lzeiml

lzeiml commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

remaining sonar issues are not relevant:
- menubar does not have to be focusable directly, at least according to W3C
- group role is needed to satisfy axe a11y check
- tabIndex on div is needed to implement desired scrolling behaviour when a lot of items are present

Comment thread packages/core/src/components/menu-item/menu-item.tsx Outdated
Comment thread packages/core/src/components/menu/menu.tsx Outdated
Comment thread packages/core/src/components/menu/menu.tsx
@lzeiml

lzeiml commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

regarding open sonar issues:
aria-description does in fact work, and it is not flagged by storybook.
<dialog> instead of <section> breaks the overlay

Comment thread .changeset/fair-falcons-search.md Outdated
Comment thread testing/visual-testing/tests/menu/link/index.html
@flxlst09 flxlst09 added this to the 5.1.0 milestone Jul 3, 2026
Comment thread packages/core/src/components/menu/menu.tsx Outdated
Comment thread packages/core/src/components/menu-category/test/menu-category.ct.ts Outdated
@sonarqubecloud

sonarqubecloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

@nuke-ellington nuke-ellington changed the title core(menu): Implement w3c menu pattern for ix-menu feat(core/menu): Implement w3c menu pattern for ix-menu Jul 7, 2026
@nuke-ellington nuke-ellington merged commit 0648d8d into main Jul 7, 2026
30 of 31 checks passed
@nuke-ellington nuke-ellington deleted the feat/4304-menu-a11y branch July 7, 2026 12:34
@github-actions github-actions Bot mentioned this pull request Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants